#ifndef _T_BUFFER_GL_H_
#define _T_BUFFER_GL_H_

#include "../interface/C_gl_Device.h"

#include "I_Buffer.h"
#include "../glCommon/glCommon.h"
#include "../glCommon/glBuffer.h"

#include <string.h>
#include <cuda_runtime.h>
#include <cuda_gl_interop.h>

//////////////////////////////////////////////////////////////////////////
/// forward declaration of classes
//////////////////////////////////////////////////////////////////////////
template <class T> class T_Buffer_GL;
template <class T> class T_Buffer2D_GL;
template <class T> class T_DoubleBuffer_GL;

//////////////////////////////////////////////////////////////////////////
/// T_glBuffer is an interface for cuda mappable objects 
/// like host memory or d3d ogl devices
//////////////////////////////////////////////////////////////////////////
template <class T>
class T_Buffer_GL : public I_Buffer
{
	friend class T_DoubleBuffer_GL<T>;
protected:
	T * m_hptr;
	T * m_dptr;

	C_gl_Buffer *m_pApiBuffer;

public:

	T_Buffer_GL()
	{
		m_hptr = NULL;
		m_dptr = NULL;
		m_pApiBuffer = NULL;
	}

	virtual ~T_Buffer_GL(void) { Release(); }

	virtual void Create(int nSize, I_api_Device * pDevice)
	{
		if (!nSize) return;

		m_size = nSize;

		m_hptr = (T * ) malloc(SizeInBytes());

		S_api_BufferDesc bufferDesc;
		bufferDesc.ByteWidth = SizeInBytes();

		S_api_Subresource_Data subresourceData;
 
		pDevice->CreateBuffer(&bufferDesc, &subresourceData, (I_api_Buffer **) &m_pApiBuffer);

		cudaError_t error = cudaGLRegisterBufferObject(*(int *)m_pApiBuffer->Get());

		if (error != cudaSuccess)
			fprintf(stderr, "%s", cudaGetErrorString(error));

		m_owner = true;

		m_offset = 0;
	}

	virtual void Release(void)
	{
		if (m_owner) 
		{
			free(m_hptr);

			cudaGLUnregisterBufferObject(*(int *)m_pApiBuffer->Get());

			delete m_pApiBuffer;
		}

		m_hptr = NULL;
		m_dptr = NULL;
		m_pApiBuffer = NULL;
		m_flag = 0;
		m_size = 0;
		m_owner = false;
	}

	virtual void Create(int32 nSize, I_Buffer * pBuffer)
	{		
		int offset = 0; 
		T * hptr = NULL;
		T * dptr = NULL;
		I_api_Buffer * pApiBuffer = NULL;

		if (pBuffer->Request(nSize, &offset, (void **) &hptr, (void **) &dptr, (void **) &pApiBuffer))
		{
			m_size = nSize;
			
			m_hptr = (T *)hptr;
			m_dptr = (T *)dptr;
			m_pApiBuffer = (C_gl_Buffer *) pApiBuffer;

			m_offset = offset;

			m_owner = false;
		}
	}

	virtual bool Request(int nSize, int * pOffset, void **hPtr, void **dPtr, void **bPtr)
	{
		if (m_owner && nSize <= m_size - m_offset && m_flag == 0)
		{
			*hPtr = m_hptr + m_offset;
			*dPtr = NULL;
			*bPtr = (void *) m_pApiBuffer;

			*pOffset = m_offset;

			m_offset += nSize;
			return true;
		}
		else return false;
	}

	virtual void *Map(int32 nFlag)
	{
		// if the buffer has already been mapped return NULL instantly
		if (m_flag) return NULL;		

		// otherwise act as in e_Map description
		switch(nFlag)
		{
			///  When you need to get data from cuda pointer
			///    Call Map(RO)
			///    Map copies data from cuda pointer to host and returns host pointer
			///    Call Unmap()
			///    Read host pointer
			///    Unmap() doesn't change anything
		case e_Map_RO: 
			m_pApiBuffer->Map(E_api_Map_Read, 0, (void **) &m_dptr);
			memcpy(m_hptr, m_dptr, SizeInBytes());
			m_pApiBuffer->Unmap();
			break;

			///  When you need to write initial values to cuda pointer
			///    Call Map(WO)
			///    Map instantly returns host pointer
			///    Write into host pointer
			///    Call Unmap ()
			///    Unmap checks that flag was WO and writes back to cuda array
		case e_Map_WO:
			break;

			///  When you need to Read cuda pointer Modify and Write
			///    Call Map(RW)
			///    Map copies data from cuda pointer to host and returns host pointer
			///    Read/Write host pointer 
			///    Call Unmap()
			///    Unmap() copies data from host pointer to cuda pointer
		case e_Map_RW:
			m_pApiBuffer->Map(E_api_Map_Read_Write, 0, (void **) &m_dptr);
			m_flag = nFlag;
			return m_dptr;
			break;

		case e_Map_Lock:
			m_flag = nFlag;
			cudaGLMapBufferObject((void **) &m_dptr, *(int *)m_pApiBuffer->Get());
			return m_dptr;
			break;

			// if anything else was called return now to avoid setting the flag
		default: return NULL;
		}

		// save flag for subsequent unmap call
		m_flag = nFlag;

		// map always returns host pointer
		return m_hptr;
	}

	virtual void Unmap()
	{
		// otherwise act as in e_Map description
		switch(m_flag)
		{
		case e_Map_RO: 
			break;

		case e_Map_WO:
			m_pApiBuffer->Map(E_api_Map_Write, 0, (void **) &m_dptr);
			memcpy(m_dptr, m_hptr, SizeInBytes());
			m_pApiBuffer->Unmap();
			m_dptr = NULL;
			break;

		case e_Map_RW:
			memcpy(m_hptr, m_dptr, SizeInBytes());
			m_pApiBuffer->Unmap();
			m_dptr = NULL;
			break;

		case e_Map_Lock:
			cudaGLUnmapBufferObject(*(int *)m_pApiBuffer->Get());
			m_dptr = NULL;
			break;

			// if anything else was called return now to avoid reseting the flag
		default: return;
		}

		// reset flag
		m_flag = 0;
	}

	virtual void Memcpy(int32 dir, int32 nStart = 0, int32 nCount = 0)
	{
		if (nCount == 0) nCount = m_size-nStart;

		switch(dir)
		{
		case e_Copy_DtoH:
			if (m_flag == e_Map_Lock)
				cudaMemcpy(m_hptr+nStart, m_dptr+nStart, nCount*sizeof(T), cudaMemcpyDeviceToHost);
			else if (m_flag == 0)
			{
				m_pApiBuffer->Map(E_api_Map_Read, 0, (void **) &m_dptr);

				memcpy(m_hptr+nStart, m_dptr+nStart, nCount*sizeof(T));

				m_pApiBuffer->Unmap();
				m_dptr = NULL;
			}
			break;
		case e_Copy_HtoD:
			if (m_flag == e_Map_Lock)
				cudaMemcpy(m_dptr+nStart, m_hptr+nStart, nCount*sizeof(T), cudaMemcpyHostToDevice);
			else if (m_flag == 0)
			{
				m_pApiBuffer->Map(E_api_Map_Write, 0, (void **) &m_dptr);

				memcpy(m_dptr+nStart, m_hptr+nStart, nCount*sizeof(T));

				m_pApiBuffer->Unmap();

				m_dptr = NULL;
			}

			break;

		default: 
			break;
		}
	}

	virtual void Memset(void *ptr, int32 nStart = 0, int32 nCount = 0)
	{
		if (nCount == 0) nCount = m_size-nStart;

		T t;

		if (ptr == NULL)
			memset(&t, 0, sizeof(T));
		else
			memcpy(&t, ptr, sizeof(T));

		for (int i = nStart; i < nStart + nCount; i++)
			m_hptr[i] = t;

		if (m_flag == e_Map_Lock)
			cudaMemcpy(m_dptr+nStart, m_hptr+nStart, nCount*sizeof(T), cudaMemcpyHostToDevice);
		else if (m_flag == 0)
		{
			m_pApiBuffer->Map(E_api_Map_Write, 0, (void **) &m_dptr);
			memcpy(m_dptr+nStart, m_hptr+nStart, nCount*sizeof(t));
			m_pApiBuffer->Unmap();
		}
	}

	virtual void	* hPtr(void) { return m_hptr; }
	virtual void    hPtr(void *ptr) { m_hptr = (T * ) ptr; }

	virtual void	* dPtr(void) { return m_dptr; }
	virtual void		dPtr(void *ptr) { m_dptr = (T * ) ptr; }

	virtual I_api_Buffer	* bufferPtr(void) { return m_pApiBuffer; }
	virtual void						bufferPtr(I_api_Buffer	* ptr) { m_pApiBuffer = (C_gl_Buffer *) ptr; }

	virtual int SizeInBytes(void) { return m_size * sizeof(T); }
};

#endif // _T_GL_BUFFER_H_